/*
 * Inline Form Validation Engine 2.2, jQuery plugin
 *
 * Copyright(c) 2010, Cedric Dugas
 * http://www.position-absolute.com
 *
 * 2.0 Rewrite by Olivier Refalo
 * http://www.crionics.com
 *
 * Form validation engine allowing custom regex rules to be added.
 * Licensed under the MIT License
 */
(function($) {

	var methods = {

		/**
		 * Kind of the constructor, called before any action
		 * 
		 * @param {Map}
		 *            user options
		 */
		init : function(options) {
			var form = this;
			if (!form.data('jqv') || form.data('jqv') == null) {
				methods._saveOptions(form, options);

				// bind all formError elements to close on click
				$(".formError").live("click", function() {
					$(this).fadeOut(150, function() {

						// remove prompt once invisible
						$(this).remove();
					});
				});
			}
		},
		/**
		 * Attachs jQuery.validationEngine to form.submit and field.blur events
		 * Takes an optional params: a list of options ie.
		 * jQuery("#formID1").validationEngine('attach', {promptPosition :
		 * "centerRight"});
		 */
		attach : function(userOptions) {

			var form = this;
			var options;

			if (userOptions)
				options = methods._saveOptions(form, userOptions);
			else
				options = form.data('jqv');

			var validateAttribute = (form
					.find("[data-validation-engine*=validate]")) ? "data-validation-engine"
					: "class";

			if (!options.binded) {
				if (options.bindMethod == "bind") {

					// bind fields
					form.find("[class*=validate]").not("[type=checkbox]").not(
							"[type=radio]").not(".datepicker").bind(
							options.validationEventTrigger,
							methods._onFieldEvent);
					form
							.find(
									"[class*=validate][type=checkbox],[class*=validate][type=radio]")
							.bind("click", methods._onFieldEvent);

					form.find("[class*=validate][class*=datepicker]").bind(
							options.validationEventTrigger, {
								"delay" : 300
							}, methods._onFieldEvent);

					// bind form.submit
					form.bind("submit", methods._onSubmitEvent);
				} else if (options.bindMethod == "live") {
					// bind fields with LIVE (for persistant state)
					form.find("[class*=validate]").not("[type=checkbox]").not(
							".datepicker").live(options.validationEventTrigger,
							methods._onFieldEvent);
					form.find("[class*=validate][type=checkbox]").live("click",
							methods._onFieldEvent);

					form.find("[class*=validate][class*=datepicker]").live(
							options.validationEventTrigger, {
								"delay" : 300
							}, methods._onFieldEvent);

					// bind form.submit
					form.live("submit", methods._onSubmitEvent);
				}

				options.binded = true;
			}
			return this;
		},
		/**
		 * Unregisters any bindings that may point to jQuery.validaitonEngine
		 */
		detach : function() {
			var form = this;
			var options = form.data('jqv');
			if (options.binded) {

				// unbind fields
				form.find("[class*=validate]").not("[type=checkbox]").unbind(
						options.validationEventTrigger, methods._onFieldEvent);
				form
						.find(
								"[class*=validate][type=checkbox],[class*=validate][type=radio]")
						.unbind("click", methods._onFieldEvent);

				// unbind form.submit
				form.unbind("submit", methods.onAjaxFormComplete);

				// unbind live fields (kill)
				form.find("[class*=validate]").not("[type=checkbox]").die(
						options.validationEventTrigger, methods._onFieldEvent);
				form.find("[class*=validate][type=checkbox]").die("click",
						methods._onFieldEvent);
				// unbind form.submit

				form.die("submit", methods.onAjaxFormComplete);

				form.removeData('jqv');
			}
		},
		/**
		 * Validates the form fields, shows prompts accordingly. Note: There is
		 * no ajax form validation with this method, only field ajax validation
		 * are evaluated
		 * 
		 * @return true if the form validates, false if it fails
		 */
		validate : function() {
			return methods._validateFields(this);
		},
		/**
		 * Validates one field, shows prompt accordingly. Note: There is no ajax
		 * form validation with this method, only field ajax validation are
		 * evaluated
		 * 
		 * @return true if the form validates, false if it fails
		 */
		validateField : function(el) {
			var options = $(this).data('jqv');
			return methods._validateField($(el), options);
		},
		/**
		 * Validates the form fields, shows prompts accordingly. Note: this
		 * methods performs fields and form ajax validations(if setup)
		 * 
		 * @return true if the form validates, false if it fails, undefined if
		 *         ajax is used for form validation
		 */
		validateform : function() {
			return methods._onSubmitEvent.call(this);
		},
		/**
		 * Redraw prompts position, useful when you change the DOM state when
		 * validating
		 */
		updatePromptsPosition : function() {
			var form = this.closest('form');
			var options = form.data('jqv');
			// No option, take default one
			form.find('[class*=validate]').not(':hidden').not(":disabled")
					.each(
							function() {
								var field = $(this);

								var prompt = methods._getPrompt(field);
								var promptText = $(prompt).find(
										".formErrorContent").html();

								if (prompt)
									methods._updatePrompt(field, $(prompt),
											promptText, undefined, false,
											options);
							})
		},
		/**
		 * Displays a prompt on a element. Note that the element needs an id!
		 * 
		 * @param {String}
		 *            promptText html text to display type
		 * @param {String}
		 *            type the type of bubble: 'pass' (green), 'load' (black)
		 *            anything else (red)
		 * @param {String}
		 *            possible values topLeft, topRight, bottomLeft,
		 *            centerRight, bottomRight
		 */
		showPrompt : function(promptText, type, promptPosition, showArrow) {

			var form = this.closest('form');
			var options = form.data('jqv');
			// No option, take default one
			if (!options)
				options = methods._saveOptions(this, options);
			if (promptPosition)
				options.promptPosition = promptPosition;
			options.showArrow = showArrow == true;

			methods._showPrompt(this, promptText, type, false, options);
		},
		/**
		 * Closes all error prompts on the page
		 */
		hidePrompt : function() {
			var promptClass = "." + methods._getClassName($(this).attr("id"))
					+ "formError";
			$(promptClass).fadeTo("fast", 0.3, function() {
				$(this).remove();
			});
		},
		/**
		 * Closes form error prompts, CAN be invidual
		 */
		hide : function() {
			var closingtag;
			if ($(this).is("form")) {
				closingtag = "parentForm" + $(this).attr('id');
			} else {
				closingtag = $(this).attr('id') + "formError";
			}
			$('.' + closingtag).fadeTo("fast", 0.3, function() {
				$(this).remove();
			});
		},
		/**
		 * Closes all error prompts on the page
		 */
		hideAll : function() {
			$('.formError').fadeTo("fast", 0.3, function() {
				$(this).remove();
			});
		},
		/**
		 * Typically called when user exists a field using tab or a mouse click,
		 * triggers a field validation
		 */
		_onFieldEvent : function(event) {
			var field = $(this);
			var form = field.closest('form');
			var options = form.data('jqv');
			// validate the current field
			window.setTimeout(function() {
				methods._validateField(field, options);
			}, (event.data) ? event.data.delay : 0);

		},
		/**
		 * Called when the form is submited, shows prompts accordingly
		 * 
		 * @param {jqObject}
		 *            form
		 * @return false if form submission needs to be cancelled
		 */
		_onSubmitEvent : function() {
			var form = $(this);
			var options = form.data('jqv');

			// validate each field (- skip field ajax validation, no necessary
			// since we will perform an ajax form validation)
			var r = methods._validateFields(form, true);

			if (r && options.ajaxFormValidation) {
				methods._validateFormWithAjax(form, options);
				return false;
			}

			if (options.onValidationComplete) {
				options.onValidationComplete(form, r);
				return false;
			}
			return r;
		},

		/**
		 * Return true if the ajax field validations passed so far
		 * 
		 * @param {Object}
		 *            options
		 * @return true, is all ajax validation passed so far (remember ajax is
		 *         async)
		 */
		_checkAjaxStatus : function(options) {
			var status = true;
			$.each(options.ajaxValidCache, function(key, value) {
				if (!value) {
					status = false;
					// break the each
					return false;
				}
			});
			return status;
		},
		/**
		 * Validates form fields, shows prompts accordingly
		 * 
		 * @param {jqObject}
		 *            form
		 * @param {skipAjaxFieldValidation}
		 *            boolean - when set to true, ajax field validation is
		 *            skipped, typically used when the submit button is clicked
		 * 
		 * @return true if form is valid, false if not, undefined if ajax form
		 *         validation is done
		 */
		_validateFields : function(form, skipAjaxValidation) {
			var options = form.data('jqv');

			// this variable is set to true if an error is found
			var errorFound = false;

			// Trigger hook, start validation
			form.trigger("jqv.form.validating");
			// first, evaluate status of non ajax fields
			form.find('[class*=validate]').not(':hidden').not(":disabled")
					.each(
							function() {
								var field = $(this);
								errorFound |= methods._validateField(field,
										options, skipAjaxValidation);
							});
			// second, check to see if all ajax calls completed ok
			// errorFound |= !methods._checkAjaxStatus(options);

			// thrird, check status and scroll the container accordingly
			form.trigger("jqv.form.result", [ errorFound ]);

			if (errorFound) {

				if (options.scroll) {

					// get the position of the first error, there should be at
					// least one, no need to check this
					// var destination =
					// form.find(".formError:not('.greenPopup'):first").offset().top;

					// look for the visually top prompt
					var destination = Number.MAX_VALUE;
					var fixleft = 0;
					var lst = $(".formError:not('.greenPopup')");

					for ( var i = 0; i < lst.length; i++) {
						var d = $(lst[i]).offset().top;
						if (d < destination) {
							destination = d;
							fixleft = $(lst[i]).offset().left;
						}
					}

					if (!options.isOverflown)
						$("html:not(:animated),body:not(:animated)").animate({
							scrollTop : destination,
							scrollLeft : fixleft
						}, 1100);
					else {
						var overflowDIV = $(options.overflownDIV);
						var scrollContainerScroll = overflowDIV.scrollTop();
						var scrollContainerPos = -parseInt(overflowDIV.offset().top);

						destination += scrollContainerScroll
								+ scrollContainerPos - 5;
						var scrollContainer = $(options.overflownDIV
								+ ":not(:animated)");

						scrollContainer.animate({
							scrollTop : destination
						}, 1100);

						$("html:not(:animated),body:not(:animated)").animate({
							scrollTop : overflowDIV.offset().top,
							scrollLeft : fixleft
						}, 1100);
					}
				}
				return false;
			}
			return true;
		},
		/**
		 * This method is called to perform an ajax form validation. During this
		 * process all the (field, value) pairs are sent to the server which
		 * returns a list of invalid fields or true
		 * 
		 * @param {jqObject}
		 *            form
		 * @param {Map}
		 *            options
		 */
		_validateFormWithAjax : function(form, options) {

			var data = form.serialize();
			var url = (options.ajaxFormValidationURL) ? options.ajaxFormValidationURL
					: form.attr("action");
			$
					.ajax({
						type : "GET",
						url : url,
						cache : false,
						dataType : "json",
						data : data,
						form : form,
						methods : methods,
						options : options,
						beforeSend : function() {
							return options.onBeforeAjaxFormValidation(form,
									options);
						},
						error : function(data, transport) {
							methods._ajaxError(data, transport);
						},
						success : function(json) {

							if (json !== true) {

								// getting to this case doesn't necessary means
								// that the form is invalid
								// the server may return green or closing prompt
								// actions
								// this flag helps figuring it out
								var errorInForm = false;
								for ( var i = 0; i < json.length; i++) {
									var value = json[i];

									var errorFieldId = value[0];
									var errorField = $($("#" + errorFieldId)[0]);

									// make sure we found the element
									if (errorField.length == 1) {

										// promptText or selector
										var msg = value[2];
										// if the field is valid
										if (value[1] == true) {

											if (msg == "" || !msg) {
												// if for some reason,
												// status==true and error="",
												// just close the prompt
												methods
														._closePrompt(errorField);
											} else {
												// the field is valid, but we
												// are displaying a green prompt
												if (options.allrules[msg]) {
													var txt = options.allrules[msg].alertTextOk;
													if (txt)
														msg = txt;
												}
												methods._showPrompt(errorField,
														msg, "pass", false,
														options, true);
											}

										} else {
											// the field is invalid, show the
											// red error prompt
											errorInForm |= true;
											if (options.allrules[msg]) {
												var txt = options.allrules[msg].alertText;
												if (txt)
													msg = txt;
											}
											methods._showPrompt(errorField,
													msg, "", false, options,
													true);
										}
									}
								}
								options.onAjaxFormComplete(!errorInForm, form,
										json, options);
							} else
								options.onAjaxFormComplete(true, form, "",
										options);
						}
					});

		},
		/**
		 * Validates field, shows prompts accordingly
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            field's validation rules
		 * @param {Map}
		 *            user options
		 * @return true if field is valid
		 */
		_validateField : function(field, options, skipAjaxValidation) {
			if (!field.attr("id"))
				$
						.error("jQueryValidate: an ID attribute is required for this field: "
								+ field.attr("name")
								+ " class:"
								+ field.attr("class"));

			var rulesParsing = field.attr('class');
			var getRules = /validate\[(.*)\]/.exec(rulesParsing);
			if (!getRules)
				return false;
			var str = getRules[1];
			var rules = str.split(/\[|,|\]/);

			// true if we ran the ajax validation, tells the logic to stop
			// messing with prompts
			var isAjaxValidator = false;
			var fieldName = field.attr("name");
			var promptText = "";
			var required = false;
			options.isError = false;
			options.showArrow = true;

			for ( var i = 0; i < rules.length; i++) {

				var errorMsg = undefined;
				switch (rules[i]) {

				case "required":
					required = true;
					errorMsg = methods._required(field, rules, i, options);
					break;
				case "match":
					errorMsg = methods._customRegex(field, rules, i, options);
					break;
				case "groupRequired":
					// Check is its the first of group, if not, reload
					// validation with first field
					// AND continue normal validation on present field
					var classGroup = "[class*=" + rules[i + 1] + "]";
					var firstOfGroup = field.closest("form").find(classGroup)
							.eq(0);
					if (firstOfGroup[0] != field[0]) {
						methods._validateField(firstOfGroup, options,
								skipAjaxValidation)
						options.showArrow = true;
						continue;
					}
					;
					errorMsg = methods._groupRequired(field, rules, i, options);
					if (errorMsg)
						required = true;
					options.showArrow = false;
					break;
				case "ajax":
					// ajax has its own prompts handling technique
					if (!skipAjaxValidation) {
						methods._ajax(field, rules, i, options);
						isAjaxValidator = true;
					}
					break;
				case "minSize":
					errorMsg = methods._minSize(field, rules, i, options);
					break;
				case "maxSize":
					errorMsg = methods._maxSize(field, rules, i, options);
					break;
				case "min":
					errorMsg = methods._min(field, rules, i, options);
					break;
				case "max":
					errorMsg = methods._max(field, rules, i, options);
					break;
				case "inpast":
					errorMsg = methods._past(field, rules, i, options);
					break;
				case "infuture":
					errorMsg = methods._future(field, rules, i, options);
					break;
				case "dateRange":
					var classGroup = "[class*=" + rules[i + 1] + "]";
					var firstOfGroup = field.closest("form").find(classGroup)
							.eq(0);
					var secondOfGroup = field.closest("form").find(classGroup)
							.eq(1);
					/*
					 * if (firstOfGroup[0] != field[0]) {
					 * methods._validateField(firstOfGroup, options,
					 * skipAjaxValidation) options.showArrow = true; continue; };
					 */
					// if one entry out of the pair has value then proceed to
					// run through validation
					if (firstOfGroup[0].value || secondOfGroup[0].value) {
						errorMsg = methods._dateRange(firstOfGroup,
								secondOfGroup, rules, i, options);
					}
					if (errorMsg)
						required = true;
					options.showArrow = false;
					break;

				case "dateTimeRange":
					var classGroup = "[class*=" + rules[i + 1] + "]";
					var firstOfGroup = field.closest("form").find(classGroup)
							.eq(0);
					var secondOfGroup = field.closest("form").find(classGroup)
							.eq(1);
					/*
					 * if (firstOfGroup[0] != field[0]) {
					 * methods._validateField(firstOfGroup, options,
					 * skipAjaxValidation) options.showArrow = true; continue; };
					 */
					// if one entry out of the pair has value then proceed to
					// run through validation
					if (firstOfGroup[0].value || secondOfGroup[0].value) {
						errorMsg = methods._dateTimeRange(firstOfGroup,
								secondOfGroup, rules, i, options);
					}
					if (errorMsg)
						required = true;
					options.showArrow = false;
					break;
				case "maxCheckbox":
					errorMsg = methods._maxCheckbox(field, rules, i, options);
					field = $($("input[name='" + fieldName + "']"));
					break;
				case "minCheckbox":
					errorMsg = methods._minCheckbox(field, rules, i, options);
					field = $($("input[name='" + fieldName + "']"));
					break;
				case "equals":
					errorMsg = methods._equals(field, rules, i, options);
					break;
				case "funcCall":
					errorMsg = methods._funcCall(field, rules, i, options);
					break;
				case "endsWith": errorMsg = methods._endsWith(field, rules,i,options); 
					;break;
				case "notEndsWith": errorMsg = methods._notEndsWith(field,rules,i,options);
				default:
					// $.error("jQueryValidator rule not found"+rules[i]);
				}
				if (errorMsg !== undefined) {
					promptText += errorMsg + "<br/>";
					options.isError = true;

				}

			}
			// If the rules required is not added, an empty field is not
			// validated
			if (!required) {
				if (field.val() == "")
					options.isError = false;
			}

			// Hack for radio/checkbox group button, the validation go into the
			// first radio/checkbox of the group
			var fieldType = field.attr("type");

			if ((fieldType == "radio" || fieldType == "checkbox")
					&& $("input[name='" + fieldName + "']").size() > 1) {
				field = $($("input[name='" + fieldName
						+ "'][type!=hidden]:first"));
				options.showArrow = false;
			}
			if (fieldType == "text"
					&& $("input[name='" + fieldName + "']").size() > 1) {
				field = $($("input[name='" + fieldName
						+ "'][type!=hidden]:first"));
				options.showArrow = false;
			}

			if (options.isError) {

				methods._showPrompt(field, promptText, "", false, options);
			} else {
				if (!isAjaxValidator)
					methods._closePrompt(field);
			}
			field.trigger("jqv.field.result", [ field, options.isError,
					promptText ]);
			return options.isError;
		},
		/**
		 * Required validation
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_required : function(field, rules, i, options) {
			switch (field.attr("type")) {
			case "text":
			case "password":
			case "textarea":
			case "file":
			default:
				if (!field.val()) {
					// methods._getErrorMsg(rules[i],rules[i+1]);
					return options.allrules[rules[i]].alertText;
				}
				break;
			case "radio":
			case "checkbox":
				var name = field.attr("name");
				if ($("input[name='" + name + "']:checked").size() == 0) {
					if ($("input[name='" + name + "']").size() == 1)
						return options.allrules[rules[i]].alertTextCheckboxe;
					else
						return options.allrules[rules[i]].alertTextCheckboxMultiple;
				}
				break;
			// required for <select>
			case "select-one":
				// added by paul@kinetek.net for select boxes, Thank you
				if (!field.val())
					return options.allrules[rules[i]].alertText;
				break;
			case "select-multiple":
				// added by paul@kinetek.net for select boxes, Thank you
				if (!field.find("option:selected").val())
					return options.allrules[rules[i]].alertText;
				break;
			}
		},
		/**
		 * Validate that 1 from the group field is required
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_groupRequired : function(field, rules, i, options) {
			var classGroup = "[class*=" + rules[i + 1] + "]";
			var isValid = false;
			// Check all fields from the group
			field.closest("form").find(classGroup).each(function() {
				if (!methods._required($(this), rules, i, options)) {
					isValid = true;
					return false;
				}
			})

			if (!isValid)
				return options.allrules[rules[i]].alertText;
		},
		/**
		 * Validate Regex rules
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_customRegex : function(field, rules, i, options) {
			var customRule = rules[i + 1];
			var rule = options.allrules[customRule];
			var ex, alertText;
			if (!rule) {
				ex = customRule;
				alertText = '"' + field.val() + "\" doesn't match regex:[" + ex
						+ "]";
			} else {
				ex = rule.regex;
				alertText = options.allrules[customRule].alertText;
			}

			if (!ex) {
				alert("jqv:custom regex not found " + customRule);
				return;
			}
			var pattern = new RegExp(ex);

			if (!pattern.test(field.val()))
				return alertText;
		},
		/**
		 * Validate custom function outside of the engine scope
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_funcCall : function(field, rules, i, options) {
			var functionName = rules[i + 1];
			var fn = window[functionName];
			if (typeof (fn) == 'function')
				return fn(field, rules, i, options);

		},
		/**
		 * Field match
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_equals : function(field, rules, i, options) {
			var equalsField = rules[i + 1];

			if (field.val() != $("#" + equalsField).val())
				return options.allrules.equals.alertText;
		},
		/**
		 * Check the maximum size (in characters)
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_maxSize : function(field, rules, i, options) {
			var max = rules[i + 1];
			var len = field.val().length;

			if (len > max) {
				var rule = options.allrules.maxSize;
				return rule.alertText + max + rule.alertText2;
			}
		},
		/**
		 * Check the minimum size (in characters)
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_minSize : function(field, rules, i, options) {
			var min = rules[i + 1];
			var len = field.val().length;

			if (len < min) {
				var rule = options.allrules.minSize;
				return rule.alertText + min + rule.alertText2;
			}
		},
		/**
		 * Check number minimum value
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_min : function(field, rules, i, options) {
			var min = parseFloat(rules[i + 1]);
			var len = parseFloat(field.val());

			if (len < min) {
				var rule = options.allrules.min;
				if (rule.alertText2)
					return rule.alertText + min + rule.alertText2;
				return rule.alertText + min;
			}
		},
		/**
		 * Check number maximum value
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_max : function(field, rules, i, options) {
			var max = parseFloat(rules[i + 1]);
			var len = parseFloat(field.val());

			if (len > max) {
				var rule = options.allrules.max;
				if (rule.alertText2)
					return rule.alertText + max + rule.alertText2;
				// orefalo: to review, also do the translations
				return rule.alertText + max;
			}
		},
		/**
		 * Checks date is in the past
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_past : function(field, rules, i, options) {

			var p = rules[i + 1];
			var pdate = (p.toLowerCase() == "now") ? new Date() : methods
					._parseDate(p);
			var vdate = methods._parseDate(field.val());

			if (vdate < pdate) {
				var rule = options.allrules.past;
				if (rule.alertText2)
					return rule.alertText + methods._dateToString(pdate)
							+ rule.alertText2;
				return rule.alertText + methods._dateToString(pdate);
			}
		},
		/**
		 * Checks date is in the future
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_future : function(field, rules, i, options) {

			var p = rules[i + 1];
			var pdate = (p.toLowerCase() == "now") ? new Date() : methods
					._parseDate(p);
			var vdate = methods._parseDate(field.val());

			if (vdate > pdate) {
				var rule = options.allrules.future;
				if (rule.alertText2)
					return rule.alertText + methods._dateToString(pdate)
							+ rule.alertText2;
				return rule.alertText + methods._dateToString(pdate);
			}
		},
		/**
		 * Checks if valid date
		 * 
		 * @param {string}
		 *            date string
		 * @return a bool based on determination of valid date
		 */
		_isDate : function(value) {
			var dateRegEx = new RegExp(
					/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$|^(?:(?:(?:0?[13578]|1[02])(\/|-)31)|(?:(?:0?[1,3-9]|1[0-2])(\/|-)(?:29|30)))(\/|-)(?:[1-9]\d\d\d|\d[1-9]\d\d|\d\d[1-9]\d|\d\d\d[1-9])$|^(?:(?:0?[1-9]|1[0-2])(\/|-)(?:0?[1-9]|1\d|2[0-8]))(\/|-)(?:[1-9]\d\d\d|\d[1-9]\d\d|\d\d[1-9]\d|\d\d\d[1-9])$|^(0?2(\/|-)29)(\/|-)(?:(?:0[48]00|[13579][26]00|[2468][048]00)|(?:\d\d)?(?:0[48]|[2468][048]|[13579][26]))$/);
			if (dateRegEx.test(value)) {
				return true;
			}
			return false;
		},
		/**
		 * Checks if valid date time
		 * 
		 * @param {string}
		 *            date string
		 * @return a bool based on determination of valid date time
		 */
		_isDateTime : function(value) {
			var dateTimeRegEx = new RegExp(
					/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])\s+(1[012]|0?[1-9]){1}:(0?[1-5]|[0-6][0-9]){1}:(0?[0-6]|[0-6][0-9]){1}\s+(am|pm|AM|PM){1}$|^(?:(?:(?:0?[13578]|1[02])(\/|-)31)|(?:(?:0?[1,3-9]|1[0-2])(\/|-)(?:29|30)))(\/|-)(?:[1-9]\d\d\d|\d[1-9]\d\d|\d\d[1-9]\d|\d\d\d[1-9])$|^((1[012]|0?[1-9]){1}\/(0?[1-9]|[12][0-9]|3[01]){1}\/\d{2,4}\s+(1[012]|0?[1-9]){1}:(0?[1-5]|[0-6][0-9]){1}:(0?[0-6]|[0-6][0-9]){1}\s+(am|pm|AM|PM){1})$/);
			if (dateTimeRegEx.test(value)) {
				return true;
			}
			return false;
		},
		// Checks if the start date is before the end date
		// returns true if end is later than start
		_dateCompare : function(start, end) {
			return (new Date(start.toString()) < new Date(end.toString()));
		},
		/**
		 * Checks date range
		 * 
		 * @param {jqObject}
		 *            first field name
		 * @param {jqObject}
		 *            second field name
		 * @return an error string if validation failed
		 */
		_dateRange : function(first, second, rules, i, options) {
			// are not both populated
			if ((!first[0].value && second[0].value)
					|| (first[0].value && !second[0].value)) {
				return options.allrules[rules[i]].alertText
						+ options.allrules[rules[i]].alertText2;
			}

			// are not both dates
			if (!methods._isDate(first[0].value)
					|| !methods._isDate(second[0].value)) {
				return options.allrules[rules[i]].alertText
						+ options.allrules[rules[i]].alertText2;
			}

			// are both dates but range is off
			if (!methods._dateCompare(first[0].value, second[0].value)) {
				return options.allrules[rules[i]].alertText
						+ options.allrules[rules[i]].alertText2;
			}
		},

		/**
		 * Checks date time range
		 * 
		 * @param {jqObject}
		 *            first field name
		 * @param {jqObject}
		 *            second field name
		 * @return an error string if validation failed
		 */
		_dateTimeRange : function(first, second, rules, i, options) {
			// are not both populated
			if ((!first[0].value && second[0].value)
					|| (first[0].value && !second[0].value)) {
				return options.allrules[rules[i]].alertText
						+ options.allrules[rules[i]].alertText2;
			}
			// are not both dates
			if (!methods._isDateTime(first[0].value)
					|| !methods._isDateTime(second[0].value)) {
				return options.allrules[rules[i]].alertText
						+ options.allrules[rules[i]].alertText2;
			}
			// are both dates but range is off
			if (!methods._dateCompare(first[0].value, second[0].value)) {
				return options.allrules[rules[i]].alertText
						+ options.allrules[rules[i]].alertText2;
			}
		},
		/**
		 * Max number of checkbox selected
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_maxCheckbox : function(field, rules, i, options) {

			var nbCheck = rules[i + 1];
			var groupname = field.attr("name");
			var groupSize = $("input[name='" + groupname + "']:checked").size();
			if (groupSize > nbCheck) {
				options.showArrow = false;
				if (options.allrules.maxCheckbox.alertText2)
					return options.allrules.maxCheckbox.alertText + " "
							+ nbCheck + " "
							+ options.allrules.maxCheckbox.alertText2;
				return options.allrules.maxCheckbox.alertText;
			}
		},
		/**
		 * Min number of checkbox selected
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return an error string if validation failed
		 */
		_minCheckbox : function(field, rules, i, options) {

			var nbCheck = rules[i + 1];
			var groupname = field.attr("name");
			var groupSize = $("input[name='" + groupname + "']:checked").size();
			if (groupSize < nbCheck) {
				options.showArrow = false;
				return options.allrules.minCheckbox.alertText + " " + nbCheck
						+ " " + options.allrules.minCheckbox.alertText2;
			}
		},
		/**
		 * Ajax field validation
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {Array[String]}
		 *            rules
		 * @param {int}
		 *            i rules index
		 * @param {Map}
		 *            user options
		 * @return nothing! the ajax validator handles the prompts itself
		 */
		_ajax : function(field, rules, i, options) {

			var errorSelector = rules[i + 1];
			var rule = options.allrules[errorSelector];
			var extraData = rule.extraData;
			var extraDataDynamic = rule.extraDataDynamic;

			if (!extraData)
				extraData = "";

			if (extraDataDynamic) {
				var tmpData = [];
				var domIds = String(extraDataDynamic).split(",");
				for ( var i = 0; i < domIds.length; i++) {
					var id = domIds[i];
					if ($(id).length) {
						var inputValue = field.closest("form").find(id).val();
						var keyValue = id.replace('#', '') + '='
								+ escape(inputValue);
						tmpData.push(keyValue);
					}
				}
				extraDataDynamic = tmpData.join("&");
			} else {
				extraDataDynamic = "";
			}

			if (!options.isError) {
				$
						.ajax({
							type : "GET",
							url : rule.url,
							cache : false,
							dataType : "json",
							data : "fieldId=" + field.attr("id")
									+ "&fieldValue=" + field.val()
									+ "&extraData=" + extraData + "&"
									+ extraDataDynamic,
							field : field,
							rule : rule,
							methods : methods,
							options : options,
							beforeSend : function() {
								// build the loading prompt
								var loadingText = rule.alertTextLoad;
								if (loadingText)
									methods._showPrompt(field, loadingText,
											"load", true, options);
							},
							error : function(data, transport) {
								methods._ajaxError(data, transport);
							},
							success : function(json) {

								// asynchronously called on success, data is the
								// json answer from the server
								var errorFieldId = json[0];
								var errorField = $($("#" + errorFieldId)[0]);
								// make sure we found the element
								if (errorField.length == 1) {
									var status = json[1];
									// read the optional msg from the server
									var msg = json[2];
									if (!status) {
										// Houston we got a problem - display an
										// red prompt
										options.ajaxValidCache[errorFieldId] = false;
										options.isError = true;

										// resolve the msg prompt
										if (msg) {
											if (options.allrules[msg]) {
												var txt = options.allrules[msg].alertText;
												if (txt)
													msg = txt;
											}
										} else
											msg = rule.alertText;

										methods._showPrompt(errorField, msg,
												"", true, options);
									} else {
										if (options.ajaxValidCache[errorFieldId] !== undefined)
											options.ajaxValidCache[errorFieldId] = true;

										// resolves the msg prompt
										if (msg) {
											if (options.allrules[msg]) {
												var txt = options.allrules[msg].alertTextOk;
												if (txt)
													msg = txt;
											}
										} else
											msg = rule.alertTextOk;

										// see if we should display a green
										// prompt
										if (msg)
											methods._showPrompt(errorField,
													msg, "pass", true, options);
										else
											methods._closePrompt(errorField);
									}
								}
							}
						});
			}
		},
		/**
		 * Common method to handle ajax errors
		 * 
		 * @param {Object}
		 *            data
		 * @param {Object}
		 *            transport
		 */
		_ajaxError : function(data, transport) {
			if (data.status == 0 && transport == null)
				alert("The page is not served from a server! ajax call failed");
			else if (typeof console != "undefined")
				console.log("Ajax error: " + data.status + " " + transport);
		},
		/**
		 * date -> string
		 * 
		 * @param {Object}
		 *            date
		 */
		_dateToString : function(date) {

			return date.getFullYear() + "-" + (date.getMonth() + 1) + "-"
					+ date.getDate();
		},
		/**
		 * Parses an ISO date
		 * 
		 * @param {String}
		 *            d
		 */
		_parseDate : function(d) {

			var dateParts = d.split("-");
			if (dateParts == d)
				dateParts = d.split("/");
			return new Date(dateParts[0], (dateParts[1] - 1), dateParts[2]);
		},
		/**
		 * Builds or updates a prompt with the given information
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {String}
		 *            promptText html text to display type
		 * @param {String}
		 *            type the type of bubble: 'pass' (green), 'load' (black)
		 *            anything else (red)
		 * @param {boolean}
		 *            ajaxed - use to mark fields than being validated with ajax
		 * @param {Map}
		 *            options user options
		 */
		_showPrompt : function(field, promptText, type, ajaxed, options,
				ajaxform) {
			var prompt = methods._getPrompt(field);
			// The ajax submit errors are not see has an error in the form,
			// When the form errors are returned, the engine see 2 bubbles, but
			// those are ebing closed by the engine at the same time
			// Because no error was found befor submitting
			if (ajaxform)
				prompt = false;
			if (prompt)
				methods._updatePrompt(field, prompt, promptText, type, ajaxed,
						options);
			else
				methods._buildPrompt(field, promptText, type, ajaxed, options);
		},
		/**
		 * Builds and shades a prompt for the given field.
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {String}
		 *            promptText html text to display type
		 * @param {String}
		 *            type the type of bubble: 'pass' (green), 'load' (black)
		 *            anything else (red)
		 * @param {boolean}
		 *            ajaxed - use to mark fields than being validated with ajax
		 * @param {Map}
		 *            options user options
		 */
		_buildPrompt : function(field, promptText, type, ajaxed, options) {

			// create the prompt
			var prompt = $('<div>');
			prompt.addClass(methods._getClassName(field.attr("id"))
					+ "formError");
			// add a class name to identify the parent form of the prompt
			if (field.is(":input"))
				prompt.addClass("parentForm"
						+ methods._getClassName(field.parents('form')
								.attr("id")));
			prompt.addClass("formError");

			switch (type) {
			case "pass":
				prompt.addClass("greenPopup");
				break;
			case "load":
				prompt.addClass("blackPopup");
			}
			if (ajaxed)
				prompt.addClass("ajaxed");

			// create the prompt content
			var promptContent = $('<div>').addClass("formErrorContent").html(
					promptText).appendTo(prompt);
			// create the css arrow pointing at the field
			// note that there is no triangle on max-checkbox and radio
			if (options.showArrow) {
				var arrow = $('<div>').addClass("formErrorArrow");

				switch (options.promptPosition) {
				case "bottomLeft":
				case "bottomRight":
					prompt.find(".formErrorContent").before(arrow);
					arrow
							.addClass("formErrorArrowBottom")
							.html(
									'<div class="line1"><!-- --></div><div class="line2"><!-- --></div><div class="line3"><!-- --></div><div class="line4"><!-- --></div><div class="line5"><!-- --></div><div class="line6"><!-- --></div><div class="line7"><!-- --></div><div class="line8"><!-- --></div><div class="line9"><!-- --></div><div class="line10"><!-- --></div>');
					break;
				case "topLeft":
				case "topRight":
					arrow
							.html('<div class="line10"><!-- --></div><div class="line9"><!-- --></div><div class="line8"><!-- --></div><div class="line7"><!-- --></div><div class="line6"><!-- --></div><div class="line5"><!-- --></div><div class="line4"><!-- --></div><div class="line3"><!-- --></div><div class="line2"><!-- --></div><div class="line1"><!-- --></div>');
					prompt.append(arrow);
					break;
				}
			}

			// Cedric: Needed if a container is in position:relative
			// insert prompt in the form or in the overflown container?
			if (options.isOverflown)
				field.before(prompt);
			else
				$("body").append(prompt);

			var pos = methods._calculatePosition(field, prompt, options);
			prompt.css({
				"top" : pos.callerTopPosition,
				"left" : pos.callerleftPosition,
				"marginTop" : pos.marginTopSize,
				"opacity" : 0
			});

			return prompt.animate({
				"opacity" : 0.87
			});

		},
		/**
		 * Updates the prompt text field - the field for which the prompt
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {String}
		 *            promptText html text to display type
		 * @param {String}
		 *            type the type of bubble: 'pass' (green), 'load' (black)
		 *            anything else (red)
		 * @param {boolean}
		 *            ajaxed - use to mark fields than being validated with ajax
		 * @param {Map}
		 *            options user options
		 */
		_updatePrompt : function(field, prompt, promptText, type, ajaxed,
				options) {

			if (prompt) {
				if (type == "pass")
					prompt.addClass("greenPopup");
				else
					prompt.removeClass("greenPopup");

				if (type == "load")
					prompt.addClass("blackPopup");
				else
					prompt.removeClass("blackPopup");

				if (ajaxed)
					prompt.addClass("ajaxed");
				else
					prompt.removeClass("ajaxed");

				prompt.find(".formErrorContent").html(promptText);

				var pos = methods._calculatePosition(field, prompt, options);
				prompt.animate({
					"top" : pos.callerTopPosition,
					"left" : pos.callerleftPosition,
					"marginTop" : pos.marginTopSize
				});
			}
		},
		/**
		 * Closes the prompt associated with the given field
		 * 
		 * @param {jqObject}
		 *            field
		 */
		_closePrompt : function(field) {

			var prompt = methods._getPrompt(field);
			if (prompt)
				prompt.fadeTo("fast", 0, function() {
					prompt.remove();
				});
		},
		closePrompt : function(field) {
			return methods._closePrompt(field);
		},
		/**
		 * Returns the error prompt matching the field if any
		 * 
		 * @param {jqObject}
		 *            field
		 * @return undefined or the error prompt (jqObject)
		 */
		_getPrompt : function(field) {
			var className = field.attr("id").replace(":", "_") + "formError";
			var match = $("." + methods._escapeExpression(className))[0];
			if (match)
				return $(match);
		},
		/**
		 * Returns the escapade classname
		 * 
		 * @param {selector}
		 *            className
		 */
		_escapeExpression : function(selector) {
			return selector.replace(/([#;&,\.\+\*\~':"\!\^$\[\]\(\)=>\|])/g,
					"\\$1");
		},
		/**
		 * Calculates prompt position
		 * 
		 * @param {jqObject}
		 *            field
		 * @param {jqObject}
		 *            the prompt
		 * @param {Map}
		 *            options
		 * @return positions
		 */
		_calculatePosition : function(field, promptElmt, options) {

			var promptTopPosition, promptleftPosition, marginTopSize;
			var fieldWidth = field.width();
			var promptHeight = promptElmt.height();

			var overflow = options.isOverflown;
			if (overflow) {
				// is the form contained in an overflown container?
				promptTopPosition = promptleftPosition = 0;
				// compensation for the arrow
				marginTopSize = -promptHeight;
			} else {
				var offset = field.offset();
				promptTopPosition = offset.top;
				promptleftPosition = offset.left;
				marginTopSize = 0;
			}

			switch (options.promptPosition) {

			default:
			case "topRight":
				if (overflow)
					// Is the form contained in an overflown container?
					promptleftPosition += fieldWidth - 30;
				else {
					promptleftPosition += fieldWidth - 30;
					promptTopPosition += -promptHeight - 2;
				}
				break;
			case "topLeft":
				promptTopPosition += -promptHeight - 10;
				break;
			case "centerRight":
				promptleftPosition += fieldWidth + 13;
				break;
			case "bottomLeft":
				promptTopPosition = promptTopPosition + field.height() + 15;
				break;
			case "bottomRight":
				promptleftPosition += fieldWidth - 30;
				promptTopPosition += field.height() + 5;
			}

			return {
				"callerTopPosition" : promptTopPosition + "px",
				"callerleftPosition" : promptleftPosition + "px",
				"marginTopSize" : marginTopSize + "px"
			};
		},
		/**
		 * Saves the user options and variables in the form.data
		 * 
		 * @param {jqObject}
		 *            form - the form where the user option should be saved
		 * @param {Map}
		 *            options - the user options
		 * @return the user options (extended from the defaults)
		 */
		_saveOptions : function(form, options) {

			// is there a language localisation ?
			if ($.validationEngineLanguage)
				var allRules = $.validationEngineLanguage.allRules;
			else
				$
						.error("jQuery.validationEngine rules are not loaded, plz add localization files to the page");
			// --- Internals DO NOT TOUCH or OVERLOAD ---
			// validation rules and i18
			$.validationEngine.defaults.allrules = allRules;

			var userOptions = $
					.extend({}, $.validationEngine.defaults, options);

			form.data('jqv', userOptions);
			return userOptions;
		},
		/**
		 * 
		 */
		_endsWith : function(field, rules,i,options){
			var fieldContent = field.val();
			
			var pattern = new RegExp(rules[i+1]);

			if (!pattern.test(field.val()))
				return "the field doesn't end with any of these endings: " + rules[i+1].substring(3).replace("$|",", "); 
		},
		/**
		 * 
		 */
		_notEndsWith: function(field,rules,i,options){
			var fieldContent = field.val();
			
			var pattern = new RegExp(rules[i+1]);

			if (pattern.test(field.val()))
				return "the field shouldn't end with any of these endings: " + rules[i+1].substring(3).replace("$|",", ");
		},
		/**
		 * Removes forbidden characters from class name
		 * 
		 * @param {String}
		 *            className
		 */
		_getClassName : function(className) {
			return className.replace(":", "_").replace(".", "_");
		}/*
			 * , _getErrorMsg: function(preDefinedRule, userDefinedMsg){ if
			 * (userDefinedMsg.startsWitch("msg:")) { return rules[i + 1]; }
			 * else { return options.allrules[preDefinedRule].alertText; } }
			 */
	};

	/**
	 * Plugin entry point. You may pass an action as a parameter or a list of
	 * options. if none, the init and attach methods are being called. Remember:
	 * if you pass options, the attached method is NOT called automatically
	 * 
	 * @param {String}
	 *            method (optional) action
	 */
	$.fn.validationEngine = function(method) {

		var form = $(this);
		if (!form[0])
			return false; // stop here if the form does not exist

		if (typeof (method) == 'string' && method.charAt(0) != '_'
				&& methods[method]) {

			// make sure init is called once
			if (method != "showPrompt" && method != "hidePrompt"
					&& method != "hide" && method != "hideAll")
				methods.init.apply(form);

			return methods[method].apply(form, Array.prototype.slice.call(
					arguments, 1));
		} else if (typeof method == 'object' || !method) {
			// default constructor with or without arguments

			methods.init.apply(form, arguments);
			return methods.attach.apply(form);
		} else {
			$.error('Method ' + method
					+ ' does not exist in jQuery.validationEngine');
		}
	};
	// LEAK GLOBAL OPTIONS
	$.validationEngine = {
		defaults : {

			// Name of the event triggering field validation
			validationEventTrigger : "blur",
			// Automatically scroll viewport to the first error
			scroll : true,
			// Opening box position, possible locations are: topLeft,
			// topRight, bottomLeft, centerRight, bottomRight
			promptPosition : "topRight",
			bindMethod : "bind",
			// internal, automatically set to true when it parse a _ajax rule
			inlineAjax : false,
			// if set to true, the form data is sent asynchronously via ajax to
			// the form.action url (get)
			ajaxFormValidation : false,
			// Ajax form validation callback method: boolean onComplete(form,
			// status, errors, options)
			// retuns false if the form.submit event needs to be canceled.
			ajaxFormValidationURL : false,
			// The url to send the submit ajax validation (default to action)
			onAjaxFormComplete : $.noop,
			// called right before the ajax call, may return false to cancel
			onBeforeAjaxFormValidation : $.noop,
			// Stops form from submitting and execute function assiciated with
			// it
			onValidationComplete : false,

			// Used when the form is displayed within a scrolling DIV
			isOverflown : false,
			overflownDIV : "",

			// true when form and fields are binded
			binded : false,
			// set to true, when the prompt arrow needs to be displayed
			showArrow : true,
			// did one of the validation fail ? kept global to stop further ajax
			// validations
			isError : false,
			// Caches field validation status, typically only bad status are
			// created.
			// the array is used during ajax form validation to detect issues
			// early and prevent an expensive submit
			ajaxValidCache : {}

		}
	}

})(jQuery);
